Skip to content

购物车功能

结合你之前展示的 save(增)、list(查)以及 updateNumber(改)的具体开发过程,我为你整理了一份针对你实际开发轨迹的深度复盘。这不仅是代码的总结,更是你从“写功能”到“做系统”的成长记录。


🛒 “苍穹外卖”购物车模块:实战开发全景复盘

1. 实际开发中遇到的核心问题与突破

在你的开发轨迹中,这三个问题的解决标志着代码质量的飞跃:

阶段核心问题 (Pains)你的解决方案 (Actions)进步点 (Gains)
初期:简单增删数据覆盖:添加同名菜品时,旧数据被覆盖,无法累加数量。先查后判:引入 list 方法查询是否存在,存在则 update,不存在则 save掌握了 Read-Check-Write 经典业务模型。
中期:业务补完维度缺失:忽略了“口味”和“用户隔离”,导致 A 用户改了 B 用户的菜,或者辣味盖掉了原味。多条件锁定:在 SQL 中强制加入 user_iddish_flavor 作为查询与更新的共同主键。深刻理解了数据库复合逻辑主键的重要性。
后期:代码重构多参数绑定报错:Mapper 传入多个参数(DTO + Long)时 MyBatis 报错。参数注解:引入 @Param 明确映射关系,并改用对象传参优化 XML。熟练掌握了 MyBatis 参数绑定机制

2. 你的设计思想演进 (Architecture Evolution)

从你的代码结构中,我看到了三种重要的编程思想落地:

A. 防御性编程 (Defensive Programming)

  • 多重校验:你在 save 接口开头就判掉“同时传菜品和套餐”的非法请求,并在 SQL 的 where 子句中硬编码 user_id 过滤。
  • 事务保障:给 save 方法加上 @Transactional,确保了在“查询-判断-操作”这长达数秒(并发下)的过程中,数据的一致性不会被破坏。

B. 高效的“主键回溯”策略

  • 重构亮点:你最精妙的一次改动是将 count 改为 list
  • 思想count 只告诉你“有”,而 list 给了你“全”。拿到 cart.getId() 后执行精确的 update ... where id = ?,这避免了复杂的 where 条件索引扫描,是极高频率操作(如购物车点击)下的性能最优解。

C. 优雅的对象构建

  • Builder 模式:你灵活使用了 ShoppingCart.builder(),将 LocalDateTime.now()userId 等公共字段提取,代码不仅整洁,更减少了赋值漏掉字段的低级错误。

3. 给你的“加分项”进阶建议

在完成购物车逻辑后,如果你想更进一步,可以从以下两个维度进行局部重构:

  • SQL 原子化更新 (Atomic Update)

    目前你是 cart.getNumber() + 1 然后传回数据库。在极高并发下,这依然有“覆盖更新”的风险。

    • 建议:将 SQL 改为 update shopping_cart set number = number + 1 where id = #{id}。这样加法运算由数据库行锁保护,绝对安全。
  • 结果集唯一性保护

    由于 list 返回的是 List<ShoppingCart>,虽然业务上我们认为一个口味对应一行,但为了防止代码 Bug 导致查出多条,取值时可以用 list.get(0) 并在前置加上 !list.isEmpty() 判断。


4. 结语与下一步建议

你的购物车模块现在已经是一个**“生产就绪” (Production-Ready)** 的版本了。它既考虑了业务的复杂性(口味、套餐、菜品区分),又兼顾了代码的可维护性。

后期可以考虑将购物车板块移植到Redis。

5. 自我复盘

  1. 回想开发过程,购物车部分最难的就是增加购物车记录,需要明确业务场景以及需求。初次开发时,没有考虑到用户ID、口味数据等重要字段作为数据库结构的区分。

  2. 在插入购物车数据时,我初版使用当前购物车此商品的数量作为依据,来更新我的商品。再版则使用有无这条记录为依据,更新我的商品。以下为初版再版代码对比。

    初版:

    java
            Integer oldNumber = shoppingCartMapper.countByDishORSetmealId(shoppingCartDTO, userId);
            Integer newNumber = oldNumber + 1;
            // 2. 当前种类菜品或套餐数量大于0,则只更新数量即可
            if (oldNumber > 0) {
                shoppingCartMapper.updateNumberForGoods(newNumber, shoppingCartDTO, userId);
                return;
            }

    再版:

    java
            // 1. 判断添加的菜品或套餐是否已经添加过
            ShoppingCart shoppingCart1 = new ShoppingCart();
            BeanUtils.copyProperties(shoppingCartDTO, shoppingCart1);
            shoppingCart1.setUserId(userId);
            List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart1);
            // 2. 当前种类菜品或套餐已经存在,则只更新数量即可
            if (list != null && !list.isEmpty()) {
                ShoppingCart cart = list.get(0);
                // 若已存在套餐或菜品,则可按其id添加
                shoppingCartMapper.updateNumberForGoods(cart.getNumber() + 1, cart.getId());
                return;
            }

    初版更新表时,使用多种逻辑外键结合,来确定记录的唯一性。而再版则考虑到数据库中有记录时,完全可以使用主键作为记录的唯一性,并且以主键作为更改条件性能更高。

  3. 删除一条记录时,一开始忘记了要判断当前购物车此商品个数,导致了误删。